use std::collections::hash_map::{Entry, HashMap};
use std::sync::Mutex;

use once_cell::sync::Lazy;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, DeriveInput, Error, Expr, Lit, MetaNameValue, Token, Type};

struct MessageAttribute {
    result_type: Option<Type>,
}

impl Parse for MessageAttribute {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let args = Punctuated::<MetaNameValue, Token![,]>::parse_terminated(input)?;
        let mut result_type: Option<Type> = None;
        for arg in args {
            if arg.path.is_ident("result") {
                if let Expr::Lit(expr) = &arg.value {
                    if let Lit::Str(lit) = &expr.lit {
                        if let Ok(ty) = syn::parse_str::<syn::Type>(&lit.value()) {
                            result_type = Some(ty);
                        }
                    }
                }
            }
        }
        return Ok(Self { result_type });
    }
}

/// Implement an xactor message type.
///
/// The return value type defaults to (), and you can specify the type with the result parameter.
///
/// # Examples
///
/// ```ignore
/// #[message(result = "i32")]
/// struct TestMessage(i32);
/// ```
#[proc_macro_attribute]
pub fn message(args: TokenStream, input: TokenStream) -> TokenStream {
    let attr = parse_macro_input!(args as MessageAttribute);
    let mut result_type = quote! { () };
    if let Some(rt) = attr.result_type {
        result_type = quote! { #rt };
    }
    let input = parse_macro_input!(input as DeriveInput);
    let ident = &input.ident;
    let expanded = quote! {
        #input
        impl honeybee::prelude::Message for #ident {
            type Result = #result_type;
        }
    };
    expanded.into()
}

struct NetMessageAttribute {
    result_type: Option<Type>,
    message_id: Option<u16>,
}

impl Parse for NetMessageAttribute {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let args = Punctuated::<MetaNameValue, Token![,]>::parse_terminated(input)?;
        let mut result_type: Option<Type> = None;
        let mut message_id: Option<u16> = None;
        for arg in &args {
            if arg.path.is_ident("result") {
                if let Expr::Lit(expr) = &arg.value {
                    if let Lit::Str(lit) = &expr.lit {
                        if let Ok(ty) = syn::parse_str::<syn::Type>(&lit.value()) {
                            result_type = Some(ty);
                        }
                    }
                }
            }
            if arg.path.is_ident("id") {
                if let Expr::Lit(expr) = &arg.value {
                    if let Lit::Int(lit) = &expr.lit {
                        match lit.base10_parse::<u16>() {
                            Ok(id) => message_id = Some(id),
                            Err(_) => {
                                return Err(syn::Error::new_spanned(arg, "Invalid netmessage id"));
                            }
                        }
                    }
                }
            }
        }
        return Ok(Self {
            result_type,
            message_id,
        });
    }
}

/// Implement an honeybee message_id type.
///
/// # Examples
///
/// ```ignore
/// #[netmessage(id = 1, result = "u32")]
/// struct TestMessage(i32);
/// ```
#[proc_macro_attribute]
pub fn netmessage(args: TokenStream, input: TokenStream) -> TokenStream {
    static OCCUPIED_IDS: Lazy<Mutex<HashMap<u16, String>>> = Lazy::new(Mutex::default);
    let attr = parse_macro_input!(args as NetMessageAttribute);
    let mut result_type = quote! { () };
    if let Some(rt) = attr.result_type {
        result_type = quote! { #rt };
    }
    let message_id = attr.message_id;
    let input = parse_macro_input!(input as DeriveInput);
    let ident = &input.ident;

    let message_id = match message_id {
        Some(id) => id,
        None => {
            return Error::new_spanned(&ident, "message id must be provided")
                .to_compile_error()
                .into()
        }
    };

    let mut ids = OCCUPIED_IDS.lock().unwrap();
    match ids.entry(message_id) {
        Entry::Occupied(e) => {
            return Error::new_spanned(
                &ident,
                format!("message id:{} is already used by {}", message_id, e.get()),
            )
            .to_compile_error()
            .into()
        }
        Entry::Vacant(e) => {
            e.insert(ident.to_string());
        }
    }

    let expanded = quote! {
        #[derive(Serialize, Deserialize, Debug)]
        #input

        impl honeybee::prelude::Message for #ident {
            type Result = #result_type;
        }

        impl honeybee::prelude::MessageId for #ident {
            const ID: u16 = #message_id;
        }
    };
    expanded.into()
}

#[proc_macro_derive(UserMessageId)]
pub fn usermessageid_derive(input: TokenStream) -> TokenStream {
    static OCCUPIED_IDS: Lazy<Mutex<HashMap<u16, String>>> = Lazy::new(Mutex::default);
    static CONFIG: Lazy<Result<toml::Value, String>> = Lazy::new(|| {
        std::fs::read_to_string("./protobuf/message_id.toml")
            .map_err(|e| e.to_string())
            .and_then(|context| context.parse::<toml::Value>().map_err(|e| e.to_string()))
    });

    let input = parse_macro_input!(input as DeriveInput);
    let ident = &input.ident;

    let config = match CONFIG.as_ref() {
        Ok(config) => config,
        Err(err) => {
            return Error::new_spanned(&ident, format!("user message auto derive error:{}", err))
                .to_compile_error()
                .into()
        }
    };

    let message_id = config
        .get(ident.to_string())
        .and_then(|value| value.as_integer())
        .map(|id| id as u16);

    let message_id = match message_id {
        Some(id) => id,
        None => {
            //return Error::new_spanned(&ident, format!("user message id is not set for {}", ident))
            //    .to_compile_error()
            //    .into()

            let expanded = quote! {};

            return expanded.into();
        }
    };

    let mut ids = OCCUPIED_IDS.lock().unwrap();
    match ids.entry(message_id) {
        Entry::Occupied(e) => {
            return Error::new_spanned(
                &ident,
                format!(
                    "user message id:{} is already used by {}",
                    message_id,
                    e.get()
                ),
            )
            .to_compile_error()
            .into()
        }
        Entry::Vacant(e) => {
            e.insert(ident.to_string());
        }
    }

    let expanded = quote! {
        impl honeybee::prelude::MessageId for #ident {
            const ID: u16 = #message_id;
        }
    };
    expanded.into()
}
